/*
 * Copyright (c) 2010, 2016, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */
package com.machinepublishers.glass.ui.monocle;

import java.io.File;
import java.lang.reflect.Constructor;
import java.nio.ByteBuffer;
import java.nio.IntBuffer;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.Arrays;
import java.util.concurrent.atomic.AtomicBoolean;

import com.sun.glass.ui.Application;
import com.sun.glass.ui.CommonDialogs.ExtensionFilter;
import com.sun.glass.ui.CommonDialogs.FileChooserResult;
import com.sun.glass.ui.Cursor;
import com.sun.glass.ui.Pixels;
import com.sun.glass.ui.Robot;
import com.sun.glass.ui.Screen;
import com.sun.glass.ui.Size;
import com.sun.glass.ui.Timer;
import com.sun.glass.ui.View;
import com.sun.glass.ui.Window;

import javafx.scene.control.TextInputDialog;

public final class MonocleApplication extends Application {

  private final NativePlatform platform = NativePlatformFactory.getNativePlatform();
  private final RunnableProcessor runnableProcessor = platform.getRunnableProcessor();

  /** Bit to indicate that a device has touch support */
  private static final int DEVICE_TOUCH = 0;
  /** Bit to indicate that a device has multitouch support */
  private static final int DEVICE_MULTITOUCH = 1;
  /** Bit to indicate that a device has relative motion pointer support */
  private static final int DEVICE_POINTER = 2;
  /** Bit to indicate that a device has arrow keys and a select key */
  private static final int DEVICE_5WAY = 3;
  /** Bit to indicate that a device has a full PC keyboard */
  private static final int DEVICE_PC_KEYBOARD = 4;
  /** Largest bit used in device capability bitmasks */
  private static final int DEVICE_MAX = 4;
  /** A running count of the numbers of devices with each device capability */
  private int[] deviceFlags = new int[DEVICE_MAX + 1];
  private Thread shutdownHookThread;
  private Runnable renderEndNotifier = () -> platform.getScreen().swapBuffers();

  MonocleApplication() {
    //    for (InputDevice device : platform.getInputDeviceRegistry().getInputDevices()) {
    //      updateDeviceFlags(device, true);
    //    }
    //    platform.getInputDeviceRegistry().getInputDevices().addListener(
    //        (SetChangeListener<InputDevice>) change -> {
    //          if (change.wasAdded()) {
    //            InputDevice device = change.getElementAdded();
    //            updateDeviceFlags(device, true);
    //          } else if (change.wasRemoved()) {
    //            InputDevice device = change.getElementRemoved();
    //            updateDeviceFlags(device, false);
    //          }
    //        });
  }

  private void updateDeviceFlags(InputDevice device, boolean added) {
    int modifier = added ? 1 : -1;
    if (device.isTouch()) {
      deviceFlags[DEVICE_TOUCH] += modifier;
    }
    if (device.isMultiTouch()) {
      deviceFlags[DEVICE_MULTITOUCH] += modifier;
    }
    if (device.isRelative()) {
      deviceFlags[DEVICE_POINTER] += modifier;
      if (deviceFlags[DEVICE_POINTER] >= 1 && added) {
        staticCursor_setVisible(true);
      } else if (deviceFlags[DEVICE_POINTER] < 1 && !added) {
        staticCursor_setVisible(false);
      }
    }
    if (device.isFullKeyboard()) {
      deviceFlags[DEVICE_PC_KEYBOARD] += modifier;
    }
    if (device.is5Way()) {
      deviceFlags[DEVICE_5WAY] += modifier;
    }
  }

  @Override
  protected void runLoop(Runnable launchable) {
    runnableProcessor.invokeLater(launchable);
    long stackSize = AccessController.doPrivileged(
        (PrivilegedAction<Long>) () -> Long.getLong("monocle.stackSize", 0));
    Thread t = new Thread(
        new ThreadGroup("Event"),
        runnableProcessor,
        "Event Thread",
        stackSize);
    setEventThread(t);
    t.start();
    shutdownHookThread = new Thread("Monocle shutdown hook") {
      @Override
      public void run() {
        platform.shutdown();
      }
    };
    Runtime.getRuntime().addShutdownHook(shutdownHookThread);
  }

  @Override
  protected void _invokeAndWait(Runnable runnable) {
    runnableProcessor.invokeAndWait(runnable);
  }

  @Override
  protected void _invokeLater(Runnable runnable) {
    runnableProcessor.invokeLater(runnable);
  }

  @Override
  protected Object _enterNestedEventLoop() {
    return runnableProcessor.enterNestedEventLoop();
  }

  @Override
  protected void _leaveNestedEventLoop(Object retValue) {
    runnableProcessor.leaveNestedEventLoop(retValue);
  }

  @Override
  public Window createWindow(Window owner, Screen screen, int styleMask) {
    return new MonocleWindow(owner, screen, styleMask);
  }

  @Override
  public Window createWindow(long parent) {
    return new MonocleWindow(parent);
  }

  @Override
  public View createView() {
    return new MonocleView();
  }

  @Override
  public Cursor createCursor(int type) {
    return new MonocleCursor(type);
  }

  @Override
  public Cursor createCursor(int x, int y, Pixels pixels) {
    return new MonocleCursor(x, y, pixels);
  }

  @Override
  protected void staticCursor_setVisible(boolean visible) {
    NativeCursor cursor = NativePlatformFactory.getNativePlatform().getCursor();
    cursor.setVisibility(deviceFlags[DEVICE_POINTER] >= 1 ? visible : false);
  }

  @Override
  protected Size staticCursor_getBestSize(int width, int height) {
    NativeCursor cursor = NativePlatformFactory.getNativePlatform().getCursor();
    return cursor.getBestSize();
  }

  @Override
  public Pixels createPixels(int width, int height, ByteBuffer data) {
    return new MonoclePixels(width, height, data);
  }

  @Override
  public Pixels createPixels(int width, int height, IntBuffer data) {
    return new MonoclePixels(width, height, data);
  }

  //  @Override
  public Pixels createPixels(int width, int height, IntBuffer data,
      float scalex, float scaley) {
    return new MonoclePixels(width, height, data);//Java 8 and Java 9 constructors conflict--work around: use a constructor they both have
  }

  //  @Override
  public Pixels createPixels(int width, int height, IntBuffer data, float scale) {
    return new MonoclePixels(width, height, data);//Java 8 and Java 9 constructors conflict--work around: use a constructor they both have
  }

  @Override
  protected int staticPixels_getNativeFormat() {
    return platform.getScreen().getNativeFormat();
  }

  @Override
  public Robot createRobot() {
    return new MonocleRobot();
  }

  @Override
  protected double staticScreen_getVideoRefreshPeriod() {
    return 0.0;
  }

  @Override
  protected Screen[] staticScreen_getScreens() {
    Screen screen = null;
    try {
      NativeScreen ns = platform.getScreen();
      final AtomicBoolean java9 = new AtomicBoolean();
      Constructor c = AccessController.doPrivileged(
          new PrivilegedAction<Constructor>() {
            @Override
            public Constructor run() {
              try {
                Constructor c = Screen.class.getDeclaredConstructor(
                    Long.TYPE,
                    Integer.TYPE,
                    Integer.TYPE, Integer.TYPE,
                    Integer.TYPE, Integer.TYPE,
                    Integer.TYPE, Integer.TYPE,
                    Integer.TYPE, Integer.TYPE,
                    Integer.TYPE, Integer.TYPE, Float.TYPE);
                c.setAccessible(true);
                return c;
              } catch (Exception e) {
                try {
                  Constructor c = Screen.class.getDeclaredConstructor(
                      Long.TYPE,
                      Integer.TYPE,
                      Integer.TYPE, Integer.TYPE,
                      Integer.TYPE, Integer.TYPE,
                      Integer.TYPE, Integer.TYPE,
                      Integer.TYPE, Integer.TYPE,
                      Integer.TYPE, Integer.TYPE,
                      Integer.TYPE, Integer.TYPE,
                      Integer.TYPE, Integer.TYPE,
                      Float.TYPE, Float.TYPE,
                      Float.TYPE, Float.TYPE);
                  c.setAccessible(true);
                  java9.set(true);
                  return c;
                } catch (Exception e2) {
                  e2.printStackTrace();
                  return null;
                }
              }
            }
          });
      if (c != null) {
        if (java9.get()) {
          screen = (Screen) c.newInstance(1l, // dummy native pointer;
              ns.getDepth(),
              0, 0, ns.getWidth(), ns.getHeight(),
              0, 0, ns.getWidth(), ns.getHeight(),
              0, 0, ns.getWidth(), ns.getHeight(),
              ns.getDPI(), ns.getDPI(),
              ns.getScale(), ns.getScale(),
              ns.getScale(), ns.getScale());
          // Move the cursor to the middle of the screen
          MouseState mouseState = new MouseState();
          mouseState.setX(ns.getWidth() / 2);
          mouseState.setY(ns.getHeight() / 2);
          MouseInput.getInstance().setState(mouseState, false);
        } else {
          screen = (Screen) c.newInstance(
              1l, // dummy native pointer;
              ns.getDepth(),
              0, 0, ns.getWidth(), ns.getHeight(),
              0, 0, ns.getWidth(), ns.getHeight(),
              ns.getDPI(), ns.getDPI(),
              ns.getScale());
          // Move the cursor to the middle of the screen
          MouseState mouseState = new MouseState();
          mouseState.setX(ns.getWidth() / 2);
          mouseState.setY(ns.getHeight() / 2);
          MouseInput.getInstance().setState(mouseState, false);
        }
      }
    } catch (Throwable t) {
      t.printStackTrace();
    }
    return new Screen[] { screen };
  }

  @Override
  public Timer createTimer(Runnable runnable) {
    return new MonocleTimer(runnable);
  }

  @Override
  protected int staticTimer_getMinPeriod() {
    return MonocleTimer.getMinPeriod_impl();
  }

  @Override
  protected int staticTimer_getMaxPeriod() {
    return MonocleTimer.getMaxPeriod_impl();
  }

  public boolean hasWindowManager() {
    return false;
  }

  @Override
  protected FileChooserResult staticCommonDialogs_showFileChooser(
      Window owner, String folder, String filename, String title,
      int type, boolean multipleMode,
      ExtensionFilter[] extensionFilters,
      int defaultFilterIndex) {
    TextInputDialog dialog = new TextInputDialog();
    dialog.showAndWait();
    File[] files;
    String filePaths = dialog.getEditor().getText();
    if (filePaths != null && !filePaths.isEmpty()) {
      String[] filePathParts = filePaths.split("\t");
      files = new File[filePathParts.length];
      for (int i = 0; i < filePathParts.length; i++) {
        files[i] = new File(filePathParts[i]);
      }
    } else {
      files = new File[0];
    }
    return new FileChooserResult(Arrays.asList(files), null);
  }

  @Override
  protected File staticCommonDialogs_showFolderChooser(Window owner,
      String folder,
      String title) {
    Thread.dumpStack();
    throw new UnsupportedOperationException();
  }

  @Override
  protected long staticView_getMultiClickTime() {
    return MonocleView._getMultiClickTime();
  }

  @Override
  protected int staticView_getMultiClickMaxX() {
    return MonocleView._getMultiClickMaxX();
  }

  @Override
  protected int staticView_getMultiClickMaxY() {
    return MonocleView._getMultiClickMaxY();
  }

  @Override
  protected boolean _supportsTransparentWindows() {
    return true;
  }

  @Override
  protected boolean _supportsUnifiedWindows() {
    return false;
  }

  @Override
  public boolean hasTwoLevelFocus() {
    return deviceFlags[DEVICE_PC_KEYBOARD] == 0 && deviceFlags[DEVICE_5WAY] > 0;
  }

  @Override
  public boolean hasVirtualKeyboard() {
    return deviceFlags[DEVICE_PC_KEYBOARD] == 0 && deviceFlags[DEVICE_TOUCH] > 0;
  }

  @Override
  public boolean hasTouch() {
    return deviceFlags[DEVICE_TOUCH] > 0;
  }

  @Override
  public boolean hasMultiTouch() {
    return deviceFlags[DEVICE_MULTITOUCH] > 0;
  }

  @Override
  public boolean hasPointer() {
    return deviceFlags[DEVICE_POINTER] > 0;
  }

  //@Override
  public void notifyRenderingFinished() {
    invokeLater(renderEndNotifier);
  }

  @Override
  protected void finishTerminating() {
    //if this method is getting called, we don't need the shutdown hook
    Runtime.getRuntime().removeShutdownHook(shutdownHookThread);
    setEventThread(null);
    platform.shutdown();
    super.finishTerminating();
  }

  void enterDnDEventLoop() {
    _enterNestedEventLoop();
  }

  void leaveDndEventLoop() {
    _leaveNestedEventLoop(null);
  }

  //@Override
  protected int _getKeyCodeForChar(char c) {
    return KeyInput.getInstance().getKeyCodeForChar(c);
  }

}
